<?php

defined('__MAPEXEC__') or die('No direct access allowed');

/**
 * Base class for item collections.
 */
class Maps_Collection {
  
  private $count = 0;
  private $id = '';
  private $idIndex = 0;
  private $idPrefix = 'item_';
  protected $items = array();
  private $pointer = 0;
  
  /**
   * Class constructor
   * @param   string  Item Id prefix
   * @param   string  Optional Id of this collection
   * @return  void
   */
  public function __construct($prefix=null, $id='') {
    $this->setIdPrefix($prefix);
    $this->setId($id);
  }
  
  /**
   * Adds an item to the collection. Implementing classes must define
   * a type specific public function which calls this function to ensure
   * items in this collection are all of the same type.
   * @param   mixed   Item to be added to the collection.
   * @return  void
   */
  protected function addItem($item) {
    $this->items[] = $item;
    $this->incrementCount();
  }
  
  /**
   * Creates a unique Id for this item group.
   * @param   void
   * @return  string  New, unique item Id
   */
  protected function createId() {
    $id = $this->idPrefix . $this->idIndex;
    $this->idIndex++;
    return $id;
  }
  
  protected function decrementCount() {
    $this->count--;
  }
  
  /**
   * Gets an item by its Id
   * @param   string  Item Id
   * @return  object  Item object
   */
  public function getById($id) {
    foreach ($this->items as $item) {
      if ($id == $item->getId()) {
        return $item;
      }
    }
    return null;
  }
  
  /**
   * Returns the number of items in this collection.
   * @param   void
   * @return  integer   Number of items in this collection
   */
  public function getCount() {
    return $this->count;
  }
  
  /**
   * Gets the Id for this collection.
   * @param   void
   * @return  string  Unique Id
   */
  public function getId() {
    return $this->id;
  }
  
  /**
   * Gets all the items in this collection
   * @param   void
   * @return  array   Array of items in collection
   */
  protected function getItems() {
    return $this->items;
  }
  
  /** 
   * Gets the current pointer
   * @param   void
   * @return  Integer   Current pointer, useful when using Maps_Collection::next() in a loop
   */
  public function getPointer() {
    return $this->pointer;
  }
  
  /**
   * Detects if there are items in this collection.
   * @param   void
   * @return  boolean   True if items exist.
   */
  public function hasItems() {
    return $this->getCount() > 0 ? true : false;
  }
  
  protected function incrementCount() {
    $this->count++;
  }
  
  /**
   * Gets the next item in the collection.
   * @param   void
   * @return  object  Item object or null if at end of list.
   */
  public function next() {
    if ($this->pointer < $this->getCount()) {
      return $this->items[ $this->pointer++ ];
    } else {
      $this->resetPointer();
      return null;
    }
  }
  
  /**
   * Removes an item from the collection.
   * @param   string    Item id
   * @return  boolean   True if item removed, false if not.
   */
  public function remove($id) {
    foreach ($this->items as $index => $item) {
      if ($id == $item->getId()) {
        array_splice($this->items, $index, 1);
        unset($item);
        $this->decrementCount();
        return true;
      }
    }
    return false;
  }
  
  /**
   * Resets the pointer.
   * @param   void
   * @return  void
   */
  private function resetPointer() {
    $this->pointer = 0;
  }
  
  /**
   * Sets the Id for this collection.
   * @param   string  Unique Id
   * @return  void
   */
  public function setId($id='') {
    if ( is_string($id) && !empty($id) ) {
      $this->id = $id;
    }
  }
  
  /**
   * Sets the Id prefix used for this item collection.
   * @param   string  Id prefix
   * @return  void
   */
  private function setIdPrefix($prefix='item_') {
    if ( is_string($prefix) && !empty($prefix) ) {
      $this->idPrefix = $prefix;
    }
  }
  
}

/**
 * Common functionality for many different classes.
 */
class Maps_Common {
  
  protected $load = null;
  
  /**
   * Class constructor
   * @param   string  The geocoding/exporting service to use, e.g. "google"
   * @return  void
   */
  protected function __construct() {
    $this->load = new Maps_Load();
  }
  
}

/**
 * Manages configuration variables
 */
class Maps_Config {
  
  private $vars = array();
  
  /**
   * Gets a config variable's value
   * @param   string  Config variable name
   * @return  mixed   Config variable value
   */
  public function get($name) {
    return isset($this->vars[$name]) ? $this->vars[$name] : null;
  }
  
  /**
   * Sets a config variable
   * @param   string  Config variable name
   * @param   mixed   Config variable value
   * @return  void
   */
  public function set($name, $value) {
    $this->vars[$name] = $value;
  }
  
}

/**
 * Latitude and longitude class
 */
class Maps_LatLng {
  
  private $lat = null;
  private $lng = null;
  
  /**
   * Class constructor
   * @param   float   Degrees latitude
   * @param   float   Degrees longitude
   * @return  void
   */
  public function __construct($lat, $lng) {
    $this->setLat($lat);
    $this->setLng($lng);
  }
  
  /**
   * Converts this object to a string
   * @param   void
   * @return  string  Object converted to a string
   */
  public function __toString() {
    return $this->getLat() . ',' . $this->getLng();
  }
  
  /**
   * Returns the degrees latitude
   * @param   void
   * @return  float   Degrees latitude
   */
  public function getLat() {
    return $this->lat;
  }
  
  /**
   * Returns the degrees longitude
   * @param   void
   * @return  float   Degrees longitude
   */
  public function getLng() {
    return $this->lng;
  }
  
  /**
   * Sets the degrees latitude
   * @param   float   Degrees latitude
   * @return  void
   */
  public function setLat($lat) {
    $this->lat = (float) $lat;
  }
  
  /**
   * Sets the degrees longitude
   * @param   float   Degrees longitude
   * @return  void
   */
  public function setLng($lng) {
    $this->lng = (float) $lng;
  }
  
  /**
   * Converts this object to an array with lat and lng values.
   * @param   void
   * @return  array   Array with lat and lng values
   */
  public function toArray() {
    return array(
      'lat' => $this->getLat(),
      'lng' => $this->getLng()
    );
  }
  
}

/**
 * Manages the address information for a geographic location, and
 * allows multiple methods of retrieving address information.
 */
class Maps_Location {
  
  private $address = null;
  private $fullyQualifiedAddress = null;
  private $city = null;
  private $country = null;
  private $postalCode = null;
  private $region = null;
  
  /**
   * Class constructor. Can either take a fully qualified address or individual pieces of an address.<br />
   * new Maps_Location("Address, City, Region, PostalCode, Country")<br />
   * new Maps_Location("Address, City, Region, Country")<br />
   * new Maps_Location("Address", "City", "Region", "PostalCode", "Country")<br />
   * new Maps_ocation("Address", "City", "Region", "Country")
   * @param   string  Street address, or fully qualified address
   * @param   string  City
   * @param   string  Region
   * @param   string  Postal code, or country if no postal code is given
   * @param   string  Two letter country code, or null if no postal code is passed
   * @return  void
   */
  public function __construct($address, $city=null, $region=null, $postalCode=null, $country=null) {
    
    if ( $city===null && $region===null && $postalCode===null && $country===null ) {
      // Is a fully qualified address
      if ( empty($address) ) {
        die('Error: The address is empty.');
      }
      list($address, $city, $region, $postalCode, $country) = $this->extractAddress($address);
    }
    
    if ( isset($address) ) {
      $this->address = trim($address);
    }
    if ( isset($city) ) {
      $this->city = trim($city);
    }
    if ( isset($region) ) {
      $this->region = trim($region);
    }
    if ( isset($postalCode) ) {
      $this->postalCode = trim($postalCode);
    }
    if ( isset($country) ) {
      $this->country = trim($country);
    }
  }
  
  /**
   * Object automatically converted to string.
   * @param   void
   * @return  string  Fully qualified address for this object
   */
  public function __toString() {
    return $this->getFullyQualifiedAddress();
  }
  
  /**
   * Extracts all the address pieces from a fully qualified address.
   * @param   string  Fully qualified address: "Address, City, Region[, PostalCode], Country"
   * @return  array   Array containing all the address pieces, or null if address is invalid.
   */
  private function extractAddress($address) {
    $parts = $this->ensureAddressParts( explode(',', $address) );
    
    if ( count($parts) != 5 ) {
      die('<strong>'.$address.'</strong> is not a valid address.');
    }
    
    return $parts;
  }
  
  /**
   * Ensures the array of address parts has the required pieces.
   * @param   Array   Array of address pieces
   * @return  Array   Array of address pieces with all the required pieces, null for the ones
   *                  not passed into the function.
   */
  private function ensureAddressParts($parts) {
    $address = 0;
    $city = 1;
    $region = 2;
    $postalCode = 3;
    $country = 4;
    $count = count($parts);
    
    if ($count == 4) {
      $parts[$country] = $parts[$postalCode];
      $parts[$postalCode] = null;
    } elseif ($count == 3) {
      $parts[$country] = $parts[$region];
      $parts[$postalCode] = null;
      $parts[$region] = $parts[$city];
      $parts[$city] = $parts[$address];
      $parts[$address] = null;
    } elseif ($count == 2) {
      $parts[$country] = $parts[$city];
      $parts[$region] = $parts[$address];
      $parts[$postalCode] = null;
      $parts[$city] = null;
      $parts[$address] = null;
    } elseif ($count == 1) {
      $parts[$country] = $parts[$address];
      $parts[$region] = null;
      $parts[$postalCode] = null;
      $parts[$city] = null;
      $parts[$address] = null;
    }
    
    return $parts;
  }
  
  /**
   * Gets the address.
   * @param   void
   * @return  string  Street address
   */
  public function getAddress() {
    return $this->address;
  }
  
  /**
   * Gets the city.
   * @param   void
   * @return  string  City name
   */
  public function getCity() {
    return $this->city;
  }
  
  /**
   * Gets the two letter country code
   * @param   void
   * @return  string  Two letter country code
   */
  public function getCountry() {
    return $this->country;
  }
  
  /**
   * Gets the fully qualified address from the individual pieces stored in this object.
   * @param   void
   * @return  string  Fully qualified street address.
   */
  public function getFullyQualifiedAddress() {
    if ( !isset($this->fullyQualifiedAddress) ) {
      $address = $this->getAddress();
      $city = $this->getCity();
      $region = $this->getRegion();
      $postalCode = $this->getPostalCode();
      $country = $this->getCountry();
      
      if ( isset($address) && isset($city) && isset($region) && isset($postalCode) && isset($country) )
      {
        $this->fullyQualifiedAddress = "$address, $city, $region, $postalCode, $country";
      }
      elseif ( !isset($region) )
      {
        $this->fullyQualifiedAddress = $country;
      }
      elseif ( !isset($city) )
      {
        $this->fullyQualifiedAddress = "$region, $country";
      }
      elseif ( !isset($address) )
      {
        $this->fullyQualifiedAddress = "$city, $region, $country";
      }
      elseif ( !isset($postalCode) )
      {
        $this->fullyQualifiedAddress = "$address, $city, $region, $country";
      }
      else
      {
        $this->fullyQualifiedAddress = '';
      }
      
    }
    
    return $this->fullyQualifiedAddress;
  }
  
  /**
   * Gets the postal code.
   * @param   void
   * @return  string  Postal code
   */
  public function getPostalCode() {
    return $this->postalCode;
  }
  
  /**
   * Gets the region, province or state
   * @param   void
   * @return  string  Region
   */
  public function getRegion() {
    return $this->region;
  }
  
}

/**
 * Manages an object with a Maps_Location property
 */
class Maps_LocationManager {
  
  public $location = null;
  
  /**
   * Detects if this has a location object.
   * @param   void
   * @return  boolean True if this has a location object
   */
  public function hasLocation() {
    return !empty($this->location);
  }
  
  /**
   * Sets the location property.
   * @params  Same as Maps_Location::__construct()
   * @return  void
   */
  public function setLocation($address, $city=null, $region=null, $postalCode=null, $country=null) {
    $this->location = new Maps_Location($address, $city, $region, $postalCode, $country);
  }
  
}

/**
 * Manages the loading of application files
 */
class Maps_Load {
	
	private $basepath = '';
	const DS = '/';
	
  /**
   * Class constructor
   * @param   void
   * @return  void
   */
	public function __construct() {
		$this->basepath = dirname(__FILE__);
    $this->loadPackageMap();
    $this->geocode('DB');
    $this->geocode('cache');
	}
	
  /**
   * Loads a map export related file.
   * @param   string  Class name to load: pass "Foo" to load lib/Export/Maps_Export_Foo.class.php
   * @return  bool    True if file was loaded correctly
   */
	public function export($classname) {
		$class = 'Maps_Export_'.ucfirst($classname);
		$filepath = $this->basepath . self::DS . 'Export' . self::DS . $class . '.class.php';
		return $this->loadClass($class, $filepath);
	}
	
	/**
   * Loads an icon related file.
   * @param   string  Class name to load: pass "Foo" to load lib/Icon/Maps_Icon_Foo.class.php
   * @return  bool    True if file was loaded correctly
   */
	public function icon($classname) {
		$class = 'Maps_Icon_'.ucfirst($classname);
		$filepath = $this->basepath . self::DS . 'Icon' . self::DS . $class . '.class.php';
		return $this->loadClass($class, $filepath);
	}
  
  /**
   * Checks to see if an icon class name exists.
   * @param   void
   * @return  boolean   True if it exists, false otherwise.
   */
  public function iconExists($classname) {
		$filepath = $this->basepath . self::DS . 'Icon' . self::DS . 'Maps_Icon_' . ucfirst($classname) . '.class.php';
    return file_exists($filepath);
  }
  
  /**
   * Loads a geocoding related file.
   * @param   string  Class name to load: pass "Foo" to load lib/Geocode/Maps_Geocode_Foo.class.php
   * @return  bool    True if file was loaded correctly
   */
	public function geocode($classname) {
		$class = 'Maps_Geocode_'.ucfirst($classname);
		$filepath = $this->basepath . self::DS . 'Geocode' . self::DS . $class . '.class.php';
		return $this->loadClass($class, $filepath);
	}
	
  /**
   * Ensures a specific class is loaded from a specific file.
   * @param   string  Class to load
   * @param   string  File path
   * @return  bool    True if $class was found in $filepath
   */
	private function loadClass($class, $filepath) {
		if ( class_exists($class) ) {
			return true;
		} elseif ( !file_exists($filepath) ) {
			die( new Exception("File <strong>$filepath</strong> does not exist.") );
		}
		
		require_once($filepath);
		
		if ( !class_exists($class) ) {
			die( new Exception("Class <strong>$class</strong> does not exist in $filepath.") );
		}
		
		return true;
	}
	
  /**
   * Loads all map related classes
   * @param   void
   * @return  void
   */
  public function loadPackageMap() {
    $classnames = array('common','POI','legend','polyLine','polygon','polygonManager','polyLineManager', 'POIManager');
    foreach ($classnames as $classname) {
      $this->map($classname);
    }
  }
  
	/**
   * Loads a map related file.
   * @param   string  Class name to load: pass "Foo" to load lib/Map/Maps_Map_Foo.class.php
   * @return  bool    True if file was loaded correctly
   */
	public function map($classname) {
		$class = 'Maps_Map_'.ucfirst($classname);
		$filepath = $this->basepath . self::DS . 'Map' . self::DS . $class . '.class.php';
		return $this->loadClass($class, $filepath);
	}
	
	/**
   * Loads a shared application file.
   * @param   string  Class name to load: pass "Foo" to load lib/Maps_Foo.class.php
   * @return  bool    True if file was loaded correctly
   */
	public function shared($classname) {
		$class = 'Maps_'.ucfirst($classname);
		$filepath = $this->basepath . self::DS . $class . '.class.php';
		return $this->loadClass($class, $filepath);
	}
	
}

/**
 * Provides a central place to manage the entire life cycle map objects.
 */
class Maps_Manager extends Maps_Common {
  
  private $exporters = array();
  private $geocoders = array();
  private $iconManagers = array();
  private $maps = array();
  private $mapCount = 0;
  private $serviceCount = 0;
  private $serviceDefault = -1;
  private $servicePointer = -1;
  private $services = array();
  
  /**
   * Class constructor.
   * @param   string  Geocoding/exporting service name, e.g. "google"
   * @param   string  Service API key
   * @param   string  Data source name for geocoding database
   * @param   string  Database username
   * @param   string  Database password
   * @param   array   Optional PDO config options
   * @return  void
   */
  public function __construct($service, $apiKey, $dsn, $dbuser, $dbpass, $dbconf=array()) {
    parent::__construct();
    $this->addService($service, $apiKey, true);
    $this->addService('raw');
    Maps_Geocode_DB::initialize($dsn, $dbuser, $dbpass, $dbconf);
  }
  
  /**
   * Class destructor. This should be called at the end of your script to ensure
   * the geocode database gets updated and does general cleanup of all the
   * stored objects.
   * @param   void
   * @return  void
   */
  public function __destroy() {
    Maps_Geocode_DB::instance()->__destroy();
    
    $this->maps = null;
    $this->mapCount = 0;
    $this->exporters = null;
    $this->iconManagers = null;
    
    if ( !empty($this->geocoders) ) {
      foreach ($this->geocoders as $geocoder) {
        $geocoder->__destroy();
      }
      $this->geocoders = null;
    }
    
  }
  
  /**
   * Adds a map to the manager.
   * @param   Maps_Map_Common   Map object
   * @return  string  Id of map inserted
   */
  public function addMap(Maps_Map_Common $map) {
    $this->maps[] = $map;
    $this->mapCount++;
    return $map->getId();
  }
  
  /**
   * Adds a service and API Key to the manager
   * @param   string  Service name, e.g. "google"
   * @param   string  API key for this service
   * @param   boolean Whether or not this is the default service
   * @return  void
   */
  public function addService($service, $apiKey='', $isDefault=false) {
    if ($this->getServiceIndex($service) < 0) {
      $this->services[] = array('name'=>$service, 'apiKey'=>$apiKey);
      if ($isDefault) {
        $this->serviceDefault = $this->serviceCount;
      }
      $this->serviceCount++;
    }
  }
  
  /**
   * Creates a new empty map object
   * @param   void
   * @return  object  New map object
   */
  public function createMap() {
    return new Maps_Map_Common();
  }
  
  /**
   * Batch export all maps added to this managers
   * @param   string  Optional service name. Defaults to map manager service default
   * @return  array   Exported map data as array(mapId => data)
   */
  public function exportAllMaps($service='') {
    $mapInfo = array();
    foreach ($this->maps as $map) {
      $mapInfo[ $map->getId() ] = $this->exportMap($map, $service);
    }
    return $mapInfo;
  }
  
  /**
   * Exports a map
   * @param   Maps_Map_Common   Map object to export
   * @param   string  Optional service name. Defaults to map manager service default
   */
  public function exportMap(Maps_Map_Common $map, $service='') {
    return $this->getExporter($service)->render($map, $this->getIconManager($service));
  }
  
  /**
   * Geocode addresses using all available geocoding services. If one fails, the next
   * service is used. If no coordinates are found, null is returned.
   * @params  Same parameters as Maps_Geocode_Location::__construct
   * @return  Maps_LatLng   Latitude and longitude object, or null if not found.
   */
  public function getCoordsByAddress($address, $city=null, $region=null, $postalCode=null, $country=null) {
    $serviceInfo = null;
    $latlng = null;
    
    while ( $serviceInfo = $this->nextService() ) {
      $geocoder = $this->getGeocoder($serviceInfo['name'], $serviceInfo['apiKey']);
      $latlng = $geocoder->getCoordsByAddress($address, $city, $region, $postalCode, $country);
      if ( !empty($latlng) ) {
        $geocoder = null;
        $this->servicePointer = -1;
        return $latlng;
      }
    }
    
    $geocoder = null;
    return null;
  }
  
  /**
   * Gets the default export/geocoding service.
   * @param   void
   * @return  string  Service name. Errors out when no default service exists.
   */
  public function getDefaultService() {
    if ($this->serviceDefault > -1) {
      return $this->services[$this->serviceDefault];
    } else {
      throw new Exception('A default service has not been set.');
    }
  }
  
  /**
   * Gets a map export object
   * @param   string  Geocoding/exporting service name
   * @return  object  A map export object or null if $service not supported
   */
  private function getExporter($service='') {
    if ( empty($service) ) {
      $serviceInfo = $this->getDefaultService();
      $service = $serviceInfo['name'];
    } else {
      $serviceInfo = $this->getServiceInfo($service);
    }
    
    $this->load->export('common');
    
    switch ($service) {
      
      case 'google':
        if ( !isset($this->exporters[$service]) ) {
          $this->load->export('text');
          $this->load->export('html');
          $this->load->export('google');
          $this->exporters[$service] = new Maps_Export_Google($serviceInfo['apiKey']);
        }
        return $this->exporters[$service];
        break;
      
      case 'yahoo':
        if ( !isset($this->exporters[$service]) ) {
          $this->load->export('text');
          $this->load->export('html');
          $this->load->export('yahoo');
          $this->exporters[$service] = new Maps_Export_Yahoo($serviceInfo['apiKey']);
        }
        return $this->exporters[$service];
        break;
      
      case 'raw':
        if ( !isset($this->exporters[$service]) ) {
          $this->load->export('raw');
          $this->exporters[$service] = new Maps_Export_Raw();
        }
        return $this->exporters[$service];
        break;
      
      default:
        die( new Exception("<strong>$service</strong> is not a valid map export service.") );
        break;
      
    }
    
  }
  
  /**
   * Gets a geocoding object to convert addresses to geographic points.
   * @param   string  Geocoding service API key
   * @param   string  Geocoding/exporting service name
   * @return  object  Geocoding object or null if $service not supported
   */
  public function getGeocoder($service='', $apiKey='') {
    if ( empty($service) ) {
      $serviceInfo = $this->getDefaultService();
      $service = $serviceInfo['name'];
    } else {
      $serviceInfo = $this->getServiceInfo($service);
    }
    
    if (empty($apiKey)) {
      $serviceInfo = $this->getServiceInfo($service);
      $apiKey = $serviceInfo['apiKey'];
    }
    
    if ( empty($this->geocoders[$service]) ) {
      $class = 'Maps_Geocode_' . ucfirst($service);
      $this->load->geocode('common');
      $this->load->geocode('location');
      $this->load->geocode($service);
      $this->geocoders[$service] = new $class($apiKey);
    }
    
    return $this->geocoders[$service];
  }
  
  /**
   * Gets an icon manager object
   * @param   string  Geocoding/exporting service name
   * @return  object  Icon manager object, or null if $service not supported
   */
  public function getIconManager($service='') {
    if ( empty($service) ) {
      $serviceInfo = $this->getDefaultService();
    } else {
      $serviceInfo = $this->getServiceInfo($service);
    }
    
    if ( empty($this->iconManager[ $serviceInfo['name'] ]) ) {
      $className = 'Manager_' . ucfirst($serviceInfo['name']);
      if ( $this->load->iconExists($className) ) {
        $class = "Maps_Icon_$className";
        $this->load->icon('Manager_Common');
        $this->load->icon($className);
        $this->iconManager[ $serviceInfo['name'] ] = new $class();
      } else {
        $this->iconManager[ $serviceInfo['name'] ] = null;
      }
    }
    
    return $this->iconManager[ $serviceInfo['name'] ];
  }
  
  /**
   * Retrieves a map object by the map Id
   * @param   string            Map Id
   * @return  Maps_Map_Common   Map object
   */
  public function getMapById($id) {
    foreach ($this->maps as $map) {
      if ($map->getId() == $id) {
        return $map;
      }
    }
    
    die( new Exception("<strong>$id</strong> is not a valid map Id.") );
  }
  
  /**
   * Gets the index at which a service is in the $services array.
   * @param   string  Service name, e.g. "google"
   * @return  integer Array index, or -1 if not found.
   */
  private function getServiceIndex($service) {
    foreach ($this->services as $index => $serviceInfo) {
      if ($serviceInfo['name'] == $service) {
        return $index;
      }
    }
    return -1;
  }
  
  /**
   * Gets the service info for a given service
   * @param   string  Service name, e.g. "google"
   * @return  array   Service info or null if not found
   */
  private function getServiceInfo($service) {
    $index = $this->getServiceIndex($service);
    $serviceInfo = $index > -1 ? $this->services[$index] : null;
    
    if ( empty($serviceInfo) ) {
      throw new Exception("The '$service' service has not been added to the map manager.");
    }
    
    return $serviceInfo;
  }
  
  /**
   * Gets the next service.
   * @param   void
   * @return  array   Service info, or null if end of list has been reached
   */
  public function nextService() {
    $serviceInfo = null;
    
    if ($this->servicePointer < 0) {
      // First call, start loop...
      $this->servicePointer = $this->serviceDefault + 1;
      $serviceInfo = $this->services[$this->serviceDefault];
    } elseif ($this->servicePointer != $this->serviceDefault) {
      // Not pointing to the default service
      if ($this->servicePointer < $this->serviceCount) {
        // Return next service...
        $serviceInfo = $this->services[$this->servicePointer];
        $this->servicePointer++;
      } else {
        // Reached end of list, rewind
        $serviceInfo = $this->services[0];
        $this->servicePointer = 0;
      }
    } else {
      // Back to default service, stop loop.
      $this->servicePointer = -1;
    }
    
    return $serviceInfo;
  }
  
  /**
   * Centralized exception handler function.
   * @param   boolean   True or false value.
   * @param   string    Message to show user when $condition is false
   * @return  boolean   True if condition is true.
   */
  public static function ok($condition, $message) {
    if ( !$condition ) {
      die( new Exception($message) );
    }
    return $condition;
  }
  
  /**
   * Removes a service and API Key from the manager
   * @param   string  Service name, e.g. "google"
   * @return  void
   */
  public function removeService($service) {
    $index = $this->getServiceIndex($service);
    
    if ($index > -1 && $index != $this->serviceDefault) {
      $this->services = array_splice($this->services, $index, 1);
      $this->serviceCount--;
    }
  }
  
  /**
   * Removes a map object from this manager and destroys it.
   * @param   string  Id of map to remove
   * @return  void
   */
  public function removeMap($mapId) {
    if ( empty($this->maps) ) {
      return;
    }
    
    foreach ($this->maps as $index => $map) {
      if ($map->getId() == $mapId) {
        $this->maps = array_splice($this->maps, $index, 1);
        unset($map);
        return;
      }
    }
  }
  
}

?>